Skip to content

Answer the issue, merge request, notification, push mirror and project rows of issue 1240 from GitLab's source - #1570

Merged
jmrplens merged 8 commits into
issue-1240-groups-members-ldap-variablesfrom
issue-1240-issues-mrs-projects
Oct 28, 2026
Merged

jmrplens merged 8 commits into
issue-1240-groups-members-ldap-variablesfrom
issue-1240-issues-mrs-projects

Conversation

@jmrplens

@jmrplens jmrplens commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Closes #1240 (third of three layers).

Issue 1240 lists 65 request parameters that GitLab's API pages mark for a paid tier while a lower tier's schema offers them. The two layers below built the declaration table in cmd/audit_edition_tier and answered the 30 group, member, LDAP link and group variable rows. This layer reads the last 35, on the issue, merge request, notification settings, push mirror and project pages, against GitLab's Grape params and services at v19.5.0-ee, and gives each one an answer: a tier tag, a description, a declaration with the reason, or, where no tier honors the parameter at all, its removal from the input.

What changes

Twenty-seven of the rows take a tier tag, so the tier filter prunes them from the lower tiers' schemas, because GitLab drops or refuses them below their tier:

  • approver_ids on merge_request.list, list_global and list_group, Premium. Enterprise Edition applies the filter on every tier, but it matches approval rules, which a project gets only through routes refused without merge_request_approvers, so below Premium it finds the rules left from a licensed time and None answers every merge request.
  • new_epic on the account-wide and group notification updates, Premium: stored on every tier, but an epic exists only from Premium.
  • On both project creates, external_authorization_classification_label, use_custom_template, template_project_id and group_with_project_templates_id, Premium; on the update, the classification label, allow_pipeline_trigger_approve_deployment, ci_restrict_pipeline_cancellation_role, issues_template and merge_requests_template, Premium, and prevent_merge_without_jira_issue, Ultimate. The classification label, the deployment approval, the cancellation role and the Jira requirement are deleted by filter_attributes_using_license! unless the license carries their feature, and the two description templates by verify_issuable_default_templates_attrs! unless the project carries issuable_default_templates; a custom template is used only where the license carries custom_project_templates, and without it a template_project_id is refused.
  • The pull mirror settings, Premium: mirror and mirror_trigger_builds on both creates, and on the update those two, mirror_user_id, only_mirror_protected_branches, mirror_overwrites_diverged_branches and, unmarked by the page, mirror_branch_regex and import_url. Each needs admin_mirror, which needs the repository_mirrors feature: on a create, EE::Projects::CreateService#setup_pull_mirroring applies mirror and mirror_trigger_builds only for a caller who has it, and on the update verify_mirror_attrs! deletes every pull mirror attribute from a caller who has not. repository_mirrors is also a registration feature, so a Free self-managed Enterprise Edition instance with Service Ping and registration features turned on honors every one of these. The tags follow its license tier all the same, as the pull mirror actions beside them (project.pull_mirror_get, pull_mirror_configure and start_mirroring), the group wiki and the group webhooks already do here. auto_duo_code_review_enabled below is answered the other way because it follows the group Duo settings, which the layer below kept on every tier with a description, since a Free group with GitLab Credits keeps them on GitLab.com.

Three parameters stop being offered on any tier, because GitLab honors them on none:

  • new_epic on a project's notification update. The page documents the group and project updates in one table, and only the group's declares the flag (EE::NotificationSetting::EMAIL_EVENTS_MAPPING), so Grape dropped it from every project update. The flag moved out of the events the three inputs share into an epicEvent only the account-wide and group inputs embed.
  • repository_checksum_failed and wiki_checksum_failed on the five project lists. Four of their routes declare both and nothing reads them, so a list asked for the failed checksums answered every project; a user's contributed projects declare neither, nor any list filter but order_by, sort and simple, so Grape dropped both there.

The struct audit's acceptedMissingInputs records why those inputs leave out fields client-go's options carry.

Two rows keep the parameter on every tier with a description that says which part needs the tier:

  • assignee_ids on issue.create: every tier takes it, and below Premium, on an unlicensed instance or a Free GitLab.com namespace, GitLab keeps only the first assignee (IssuableBaseService#filter_assignees_using_checks; on GitLab.com the namespace's plan decides). issue.update goes through the same filter and says the same.
  • auto_duo_code_review_enabled on project.update: kept where Duo code review is available to the project, which needs a Premium license (not the Ultimate the row says) with GitLab Duo, or, on GitLab.com, a Free group with GitLab Credits, so a tag would take it from that group.

Three rows are not gated, and the declaration table records the source line that says so: mirror_branch_regex on the push mirror create and update, and include_hidden on project.list, which Enterprise Edition applies on every tier, the latter for an administrator. Community Edition drops both, as it drops every Enterprise Edition parameter, which the tier model, where Free covers both editions, cannot tell apart.

The upstream register gains row 142 for the rows where GitLab's page disagrees with its code, across the whole issue, and the contributor page of the command says how a parameter no tier honors is answered. A documentation merge request for the rows the source settles is drafted and not sent; the rows where Enterprise Edition applies a parameter its page sells as Premium are a question for GitLab before they are a merge request.

How I tested it

The tests came first, and each failed on the parent. TestCentralTierFilter_MergeRequestApproverFilter_IsOfferedFromPremium, TestCentralTierFilter_EpicNotification_IsOfferedWhereGitLabTakesIt, TestCentralTierFilter_ProjectSettings_AreOfferedFromTheTierGitLabHonorsThem and TestCentralTierFilter_ProjectChecksumFilters_AreOfferedOnNoTier build the Free, Premium and Ultimate catalogs and hold 49 parameters to the tiers their schema is offered at, those that must stay on Free and those offered on no tier among them; on the parent 3, 3, 25 and 10 of their cases failed. TestRunMain_RealTable_AnswersTheRowsGitLabsSourceSettles gains the rows of the five pages and holds that none keeps an open finding and that each declaration answers its row with its category; every one of the 35 rows was open on the parent. TestUpdateSettingsForGroup_NewEpic_SendsTheFlagAlone holds the group handler to sending the flag the shared builder no longer copies, and the project option builder tests hold that a list built with every filter sends neither checksum filter.

A new CE scenario, TestIssueCreate_TwoAssignees_OnTheFreeTier_KeepsOnlyTheFirst, creates an issue for two project members on every surface of an unlicensed instance and holds the answer to the first, which is what the description now promises. It compiles here and runs with the CE suite at the top of the stack, and CLAUDE.md and the repository map now count it among the seven files of the CE package. The other rows rest on what a Free Enterprise Edition instance does, which no Docker instance of the suite is, so I read them from the source.

Measured offline over the API pages read on 2026-10-27, go run ./cmd/audit_edition_tier/ -offline reports no open parameter finding and 28 answered over 391 joined rows, from 35 open and 23 answered, with no stale declaration.

go test ./... -count=1 passes with GITLAB_MCP_TEST_SNAPSHOT_PARITY=deferred, and every touched package keeps 100% statement coverage: cmd/audit_edition_tier, cmd/audit_1to1/internal/structs, issues, mergerequests, notifications, projects and internal/tools. The two packages whose code changed have every condition evaluated both ways under gobco (notifications 48 of 48, projects 1386 of 1386) and no lived or uncovered mutant (30 and 681 killed). In issues and mergerequests only struct tags change, in cmd/audit_edition_tier and cmd/audit_1to1/internal/structs only declaration data, and in internal/tools only tests, so their condition and mutant records are the parent's. make check-action-ids, check-meta-descriptions, check-gateway-chars, check-surface-quality, check-doc-tool-names, check-e2e-static, check-godocs, check-test-subtests, check-test-goroutines, check-test-file-names, check-md-escaping, check-tenancy, check-dead-consts, check-sdk-context, check-closed-servers and audit-catalog-first, the R-INPUT requiredness, R-PATH and SDK scopes of cmd/audit_1to1, golangci-lint with the e2e tag on the changed packages, markdownlint, the table formatter and the link check are clean, and the e2e packages compile. The struct audit reports the same 20 missing input fields as the parent and no stale declaration.

The tool snapshots, the generated tool reference in English and Spanish, the footprint, the request inventory and the testing reference change with these schemas and are regenerated once at the top of the stack. This layer edits no hand-written file that #1517 changes; the generated artifacts both change are regenerated by whichever of the two lands second.

…swer the assignee row

Issue 1240 lists the assignee_ids row of doc/api/issues.md, which marks
the parameter of issue.create Premium and Ultimate only while every
tier's schema offers it. Read against GitLab's source at v19.5.0-ee, the
parameter is not what the tier gates: every tier takes it, and what
needs Premium is a second assignee.
IssuableBaseService#filter_assignees_using_checks keeps only the first
ID unless the issue allows multiple assignees
(app/services/issuable_base_service.rb:117-122), which Community
Edition never does and Enterprise Edition does where
multiple_issue_assignees is licensed (ee/app/models/ee/issue.rb:195-197),
a feature every Premium license carries. On GitLab.com, whose license
carries every feature, the namespace's plan decides
(ee/app/models/ee/project.rb:1876-1886), so a Free namespace keeps the
first as an unlicensed instance does.

A tag would take the parameter from a Free caller who assigns one user,
so the descriptions of issue.create and issue.update, which goes through
the same filter, say that more than one assignee needs Premium or
Ultimate and that below them, on an unlicensed instance or a Free
GitLab.com namespace, GitLab keeps the first. The declaration table of
cmd/audit_edition_tier answers the row as a value-level gate with the
source that settles it.

TestRunMain_RealTable_AnswersTheRowsGitLabsSourceSettles gains the
issue page's row, which stays an open finding on the parent. The new
CE scenario, TestIssueCreate_TwoAssignees_OnTheFreeTier_KeepsOnlyTheFirst,
creates an issue for two project members on every surface of an
unlicensed instance and holds the answer to the first of them, which is
what the description now promises.

Signed-off-by: José M. Requena Plens <mail@jmrp.io>
Issue 1240 lists the approver_ids rows of doc/api/merge_requests.md,
which mark the filter of merge_request.list, list_global and list_group
Premium and Ultimate only while every tier's schema offered it.

Read at v19.5.0-ee, Enterprise Edition declares the filter
(ee/lib/ee/api/merge_requests.rb:18-21) and applies it on every tier,
with no license check (EE::MergeRequestsFinder#by_approvers,
ee/app/finders/ee/merge_requests_finder.rb:16-20), but what it matches
is approval rules: the users of a merge request's or a project's rules,
direct or through a group (MergeRequests::ByApproversFinder,
ee/app/finders/merge_requests/by_approvers_finder.rb:71-105). A project
gets rules through the approval rule routes, which refuse an instance
without the Premium merge_request_approvers feature
(ee/lib/api/helpers/project_approval_rules_helpers.rb:68-70), so below
Premium the filter finds only the rules left from a licensed time, and
None answers every merge request. Community Edition does not declare
it. That is the shape of the group wiki filters of the layer below, so
the filter takes the Premium tag on the three list inputs, and the tier
filter prunes it from the Free schemas.

The approved-by filters stay on every tier, since approving a merge
request is a Free feature.

TestCentralTierFilter_MergeRequestApproverFilter_IsOfferedFromPremium
holds the three filters to Premium and the approved-by ones to Free,
and fails on the three on the parent;
TestRunMain_RealTable_AnswersTheRowsGitLabsSourceSettles gains the
page's three rows, all open on the parent.

Signed-off-by: José M. Requena Plens <mail@jmrp.io>
…s it, from Premium

Issue 1240 lists the new_epic rows of doc/api/notification_settings.md,
which mark the flag of the account-wide, group and project notification
updates Premium and Ultimate only while every tier's schema offered it.
The page documents the group and the project update in one table, and
GitLab's source at v19.5.0-ee answers the two scopes differently.

A project's update never takes the flag. Enterprise Edition declares
new_epic for the account-wide settings and for a group's, and not for a
project's (EE::NotificationSetting::EMAIL_EVENTS_MAPPING,
ee/app/models/ee/notification_setting.rb:7-17), the routes declare
exactly the events that mapping returns (lib/api/notification_settings.rb:41
and 108), and the live record agrees, so Grape dropped the flag from a
project's update on every tier while the schema offered it. The project
input no longer carries it.

The account-wide and group updates store it on every tier, but an epic
exists only from Premium, so the flag follows the Premium gate the epic
actions already have, as the group wiki settings of the layer below do.
It moves out of the events the three inputs share into an epicEvent
that only those two embed, tagged Premium, and their handlers copy it
onto the options the shared builder returns. The struct audit's
declaration table records why the shared events leave out a field
client-go's options carry.

TestCentralTierFilter_EpicNotification_IsOfferedWhereGitLabTakesIt
holds the flag to Premium on the account-wide and group updates and to
no tier on a project's, beside two shared events that stay on Free; on
the parent it fails on all three. TestUpdateSettingsForGroup_NewEpic_SendsTheFlagAlone
holds the group handler to sending the flag it no longer gets from the
builder, and the per-event table now sets each event on the account-wide
input, the one that carries all eighteen.
TestRunMain_RealTable_AnswersTheRowsGitLabsSourceSettles gains the
page's three rows, all open on the parent.

Signed-off-by: José M. Requena Plens <mail@jmrp.io>
… not gated

Issue 1240 lists the mirror_branch_regex rows of doc/api/remote_mirrors.md,
which mark the branch filter of project.mirror_add and project.mirror_edit
Premium and Ultimate only while every tier's schema offers it.

GitLab's source at v19.5.0-ee does not gate it. Enterprise Edition
declares the parameter on both routes with no license check
(ee/lib/ee/api/helpers/remote_mirrors_helpers.rb:11-14), lets the
services save it (ee/app/models/ee/remote_mirrors/attributes.rb:8-24)
and pushes only the branches it matches on every tier
(EE::RemoteMirror#options_for_update and #branches_to_sync,
ee/app/models/ee/remote_mirror.rb:22-35). Community Edition does not
declare it and drops it, as it drops every Enterprise Edition
parameter, which the tier model, where Free covers both editions,
cannot tell apart.

The schema stays as it is, and the declaration table answers both rows
as not gated, with the source that settles them; the rows are a
documentation fix to offer GitLab.

TestRunMain_RealTable_AnswersTheRowsGitLabsSourceSettles gains the
page's two rows, both open on the parent.

Signed-off-by: José M. Requena Plens <mail@jmrp.io>
… GitLab honors them at

Issue 1240 lists 26 rows of doc/api/projects.md that mark a parameter of
project.create, project.create_for_user, project.list or project.update
for a paid tier while every tier's schema offered it. Read against
GitLab's Grape params and services at v19.5.0-ee, 22 of them, and two
update parameters the page does not mark, are dropped or refused below
their tier, so a schema offering them there invited a call GitLab
answers with success and without the setting. They take a tier tag:

- external_authorization_classification_label on both creates and the
  update, allow_pipeline_trigger_approve_deployment and
  ci_restrict_pipeline_cancellation_role on the update, Premium, and
  prevent_merge_without_jira_issue on the update, Ultimate, which
  EE::API::Helpers::ProjectsHelpers#filter_attributes_using_license!
  deletes unless the instance's license carries
  external_authorization_service_api_management, protected_environments,
  ci_pipeline_cancellation_restrictions or
  jira_issue_association_enforcement
  (ee/lib/ee/api/helpers/projects_helpers.rb:124-142);
- use_custom_template, template_project_id and
  group_with_project_templates_id on both creates, Premium: a custom
  template is used only where the instance's license carries
  custom_project_templates
  (EE::Projects::CreateFromTemplateService#use_custom_template?,
  ee/app/services/ee/projects/create_from_template_service.rb:60-64,
  and ee/app/models/ee/application_setting.rb:908-910), and without it
  a template_project_id falls to the built-in templates and is refused;
- issues_template and merge_requests_template on the update, Premium,
  deleted unless the project carries issuable_default_templates
  (ee/lib/ee/api/projects.rb:132-137);
- mirror and mirror_trigger_builds on both creates, and on the update
  those two, mirror_user_id, only_mirror_protected_branches,
  mirror_overwrites_diverged_branches and, unmarked by the page,
  mirror_branch_regex and import_url, Premium: each is a pull mirror
  attribute, which the create applies and the update keeps only for a
  caller with admin_mirror (ee/app/services/ee/projects/create_service.rb:107-115
  and ee/lib/ee/api/projects.rb:124-130), and admin_mirror needs the
  repository_mirrors feature (ee/app/policies/ee/project_policy.rb:38
  and 757). Community Edition does not declare the update's import_url,
  so it is the mirror's URL and nothing else there.

repository_mirrors is also a registration feature of a Free
self-managed instance with Service Ping on; the settings take its
license tier, as the group wiki and group webhook ones already do here.

Two rows stay on every tier's schema, answered in the declaration table
of cmd/audit_edition_tier with the source that settles them:

- include_hidden on project.list is not gated: Enterprise Edition keeps
  it for an administrator on every tier and its finder applies it
  (ee/lib/ee/api/projects.rb:116-121 and
  ee/app/finders/ee/projects_finder.rb:49-51). Its description now says
  it is for administrators.
- auto_duo_code_review_enabled on the update is a gate on something
  other than a value. GitLab keeps it where Duo code review is available
  to the project: the instance's license must carry review_merge_request,
  a Premium feature rather than the Ultimate the row says, and the
  project must have the Duo Agent Platform's code review flow or a Duo
  Enterprise add-on, which on GitLab.com a Free group with GitLab
  Credits reaches. A tag would take it from that group, so its
  description says where it applies.

The issue's last two project rows, the checksum filters of
project.list, are the next commit's. project.create_for_user embeds the
create input, so its template_project_id, which the page does not list
for that route, takes the same tag.

TestCentralTierFilter_ProjectSettings_AreOfferedFromTheTierGitLabHonorsThem
holds 29 parameters to the tier their schema is offered from on the
three catalogs, the three that stay on Free among them, and fails on 26
assertions on the parent. TestRunMain_RealTable_AnswersTheRowsGitLabsSourceSettles
gains the page's 24 rows of these four routes, which stay open findings
on the parent.

Signed-off-by: José M. Requena Plens <mail@jmrp.io>
…where

Issue 1240 lists the repository_checksum_failed and wiki_checksum_failed
rows of doc/api/projects.md, which mark both project.list filters
Premium and Ultimate only while every tier's schema offered them.

Neither a tag nor a description answers them, because no tier honors
them. Enterprise Edition declares both on every route that takes the
project list filters (optional_filter_params_ee,
ee/lib/ee/api/helpers/projects_helpers.rb:38-42 at v19.5.0-ee), and
nothing reads them: the finder parameters the routes build carry
include_hidden and with_security_reports and neither of these
(EE::API::Helpers#project_finder_params_ee,
ee/lib/ee/api/helpers.rb:119-127), and no finder, model or service in
the tree names them. A list asked for the projects whose checksum
failed answered every project, on every tier.

The five project lists that carried them, project.list, list_forks,
list_user_projects, list_user_contributed and list_user_starred, no
longer offer either filter, and the option builder they share no longer
copies them. A user's contributed projects never took them: that route
declares order_by, sort, simple, user_id and the pagination alone
(lib/api/projects.rb:380-387), so Grape dropped both there, and the
comment on the shared filter input no longer says the three user-scoped
routes take the full filter set. The struct audit's declaration table
records why userProjectFilter leaves out two fields client-go's
ListProjectsOptions carries, as it does for the deployment merge
request options the route declares and never reads.

TestCentralTierFilter_ProjectChecksumFilters_AreOfferedOnNoTier holds
both filters out of the five lists on the three catalogs and fails on
all 30 assertions on the parent. The option builder tests now hold
that a list built with every filter a caller can set sends neither, and
TestRunMain_RealTable_AnswersTheRowsGitLabsSourceSettles gains the two
rows, open on the parent, so that no finding of the project page stays
open.

Signed-off-by: José M. Requena Plens <mail@jmrp.io>
…b's source does not gate

Answering issue 1240 read 65 rows of GitLab's API pages that mark a
request parameter for a paid tier against the source at v19.5.0-ee.
Most of them the source confirms, and the schemas took a tier tag or a
description. The rest are a page that disagrees with the code, and the
upstream register gains row 142 for them, in two groups.

In the first the source settles the row and the page is what is wrong:
the group list's marked_for_deletion_on, which Community Edition
applies; the group variable filter, read on every tier and edition;
new_epic in the table the notification page shares between a group's
update and a project's, which a project's does not declare; the two
project list checksum filters, declared and read by nothing; and the
automatic Duo code review, marked Ultimate where a Premium license with
Duo code review keeps it.

In the second Enterprise Edition applies the parameter on every tier
with no license check while the feature page sells it as Premium: the
group membership lock and compute minute limits, the push mirror branch
regex and the hidden project filter. Those are a question for GitLab
before they are a merge request, since either the page or the missing
license check can be the defect.

The contributor page of cmd/audit_edition_tier says that a parameter no
tier honors is answered by removing it from the input, with the struct
audit declaration that says why, and points the documentation fixes at
the new row.

Signed-off-by: José M. Requena Plens <mail@jmrp.io>
The issue layer adds test/e2e/gitlab/ce/issues_test.go, the scenario
that holds an issue created for two assignees on an unlicensed instance
to the first of them, and the e2e file counts in CLAUDE.md and the
repository map still said the ce package holds six. It holds seven.

Refs #1240

Signed-off-by: José M. Requena Plens <mail@jmrp.io>
@sourcery-ai

sourcery-ai Bot commented Oct 28, 2026

Copy link
Copy Markdown

Sorry @jmrplens, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 5 days and 15 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@jmrplens
jmrplens added this pull request to stack #1572 October 28, 2026 12:21
@coderabbitai

coderabbitai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 59 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 4642b95 and c32ab6a.

Files selected for processing: 15
  • CLAUDE.md
  • cmd/audit_1to1/internal/structs/analyze.go
  • cmd/audit_edition_tier/param_declarations.go
  • cmd/audit_edition_tier/param_declarations_test.go
  • docs/development/cmd-utilities.md
  • docs/development/repository-map.md
  • docs/development/upstream-bugs.md
  • internal/tools/action_catalog_test.go
  • internal/tools/issues/issues.go
  • internal/tools/mergerequests/merge_requests.go
  • internal/tools/notifications/notifications.go
  • internal/tools/notifications/notifications_test.go
  • internal/tools/projects/projects.go
  • internal/tools/projects/projects_test.go
  • test/e2e/gitlab/ce/issues_test.go
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jmrplens jmrplens added this to the 3.4.0 milestone Oct 28, 2026
@jmrplens jmrplens changed the title issue 1240 issues mrs projects Answer the issue, merge request, notification, push mirror and project rows of issue 1240 from GitLab's source Oct 28, 2026
@github-actions github-actions Bot added tooling The audit and generator commands under cmd/, and the Makefile targets that run them e2e The end-to-end suite and its Docker fixtures labels Oct 28, 2026
@sonarqubecloud

Copy link
Copy Markdown

@jmrplens
jmrplens merged commit 1b31f66 into main Oct 28, 2026
39 checks passed
@jmrplens
jmrplens deleted the issue-1240-issues-mrs-projects branch October 28, 2026 13:15
jmrplens added a commit that referenced this pull request Oct 30, 2026
Every route that takes the project list filters declares
repository_checksum_failed and wiki_checksum_failed among
optional_filter_params_ee, and nothing reads them: the finder params the
routes build carry neither, and no finder, model or service names them.
The answer to the project rows of issue 1240 (#1570) stopped offering
them on the project lists for that reason, while ListProjectsOptions
still models both, so on main R-PARAM reports each as not_offered on
GET /projects, the forks, a user's projects and a user's starred
projects: eight findings no declaration answers, where this layer
leaves none.

Each is declared declared-not-applied, with the reason the structs
scope already records for leaving them out, and
TestRun_Repository_DeclaresTheChecksumFiltersNoProjectListReads holds
the eight to the declaration; its eight subtests fail without it. The
scope reports not_offered 0 again and 1491 declared (1510 params), and
request-paths.md says so.

Signed-off-by: José M. Requena Plens <mail@jmrp.io>
jmrplens added a commit that referenced this pull request Oct 30, 2026
Every route that takes the project list filters declares
repository_checksum_failed and wiki_checksum_failed among
optional_filter_params_ee, and nothing reads them: the finder params the
routes build carry neither, and no finder, model or service names them.
The answer to the project rows of issue 1240 (#1570) stopped offering
them on the project lists for that reason, while ListProjectsOptions
still models both, so on main R-PARAM reports each as not_offered on
GET /projects, the forks, a user's projects and a user's starred
projects: eight findings no declaration answers, where this layer
leaves none.

Each is declared declared-not-applied, with the reason the structs
scope already records for leaving them out, and
TestRun_Repository_DeclaresTheChecksumFiltersNoProjectListReads holds
the eight to the declaration; its eight subtests fail without it. The
scope reports not_offered 0 again and 1491 declared (1510 params), and
request-paths.md says so.

Signed-off-by: José M. Requena Plens <mail@jmrp.io>
jmrplens added a commit that referenced this pull request Oct 30, 2026
… here sends (R-PARAM) (#1639)

Refs #1595
Refs #1596
Refs #1598
Refs #1597
Refs #1599

This is the fifth layer of the auditor stack. A param GitLab declares on
a route we already call, and that client-go's option struct does not
model, was seen by no dimension: client-go cannot send it, so no input
offers it, so no recorded request carries it. The params of row 133 of
the upstream register came from reading the diff between two records by
hand.

## What the check detects

The first commit moves the option struct reader out of R-PATH into
`cmd/audit_1to1/internal/sdkoptions`, so the always-sent check and the
new scope read one answer. A field is read with the encoder its request
uses: `json` for a `NewRequest` PATCH, POST or PUT, `url` for every
other verb and for every field of an `UploadRequest`, whose constructor
`cmd/internal/sdkroutes` now records per method. The walk goes into any
client-go struct a field carries (`[]*HookCustomHeader` is
`custom_headers[key]`), and a type defined on an option struct
(`CreateProjectForUserOptions`) is read as that struct. The always-sent
check used to judge DELETE by the `json` tag and left GET unjudged; it
now walks 614 endpoints instead of 289 and still reports the same 11
rows.

The second commit adds `-scope=params`
(`cmd/audit_1to1/internal/params`). Per route, over the union of the
actions reaching it, a param is covered when a recorded request carries
it or a request sends it on every call. The inventory records the
top-level keys of a body, so a recorded key covers a param nested under
it only where an option struct of the route models that param. Otherwise
it is `unmodeled` (nothing here can send it), `not_offered` (the struct
the action already hands client-go models it and no input offers it),
`offered_unrecorded` (an input offers it and no recorded request carries
it) or `unjudged` (the options cannot be read). A raw request is read
through the static type of its options, which the sdk scope's walk now
exports (`sdk.RawRequests`), and the redirect GitLab answers another
route with carries no options of its own. An input offers a param by
name, through the requiredness scope's aliases, through its nested
schema, or flattened with `shared.NormalizeSDKTag` (`not[author_id]` as
`not_author_id`). Path params and the attributes Workhorse fills are not
judged.

The findings are a report. The gate fails only on a declaration that
answers nothing, names no category or reason, or names a register row
that does not exist; an `upstream` declaration answers an `unmodeled`
param alone, so it goes stale the day client-go models it. The scope
reads the committed request record, so CI runs it deferred with R-GRANT
(`make audit-1to1-params`) and it is step 30 of `make analyze`.

## Witness test, red then green

`TestRun_Repository_ReportsTheParamsClientGoV3300CannotSend` pins what
the scope sees on v3.30.0: `source_project_id` and the twelve other new
filters on the four merge request lists, the deployment one included;
the system hooks' `url_variables` and `custom_headers`; `url_variables`
on the project and group hooks; the three `duo_*` flags of `PUT
/projects/:id`; `size` on a registry repository; `not[author_id]` on the
three `issues_statistics` routes and the group epics;
`include_html_description` on a release read, which `GetRelease` takes
no options to send; `approval_rules[id]` of a protected environment,
nested under the `approval_rules` the inventory records and modeled by
no field of `EnvironmentApprovalRuleOptions`; the declared `external`;
the four `duo_*` flags GitLab drops on both create routes; and the four
label collection routes as `branch-not-taken`. It also holds that a
project hook add's custom header keys are covered only through
`AddProjectHookOptions`, the struct its raw request sends, and that the
edit's stand under the `custom_headers` container
`EditProjectHookOptions` models. The bump stack changes it, which is the
proof the scope ran again.

A copy that reads the report as plain JSON fails on the parent
(47f2535a0), where no scope reports the param (`paths` names
`source_project_id` only as a response key, under `shapes`):

```text
$ go test -count=1 -run TestZZWitness -v ./cmd/audit_1to1/
    zz_witness_test.go:22: the params scope did not run: invalid scope "params" (valid: structs, actions, metadata, enums, sdk, paths, grants, required, all); nothing reports a param client-go cannot send
--- FAIL: TestZZWitness_SourceProjectIDOnTheFourMergeRequestLists (0.00s)
```

and passes here:

```text
    zz_witness_test.go:48: GET /merge_requests source_project_id unmodeled
    zz_witness_test.go:48: GET /projects/:id/merge_requests source_project_id unmodeled
    zz_witness_test.go:48: GET /groups/:id/merge_requests source_project_id unmodeled
    zz_witness_test.go:48: GET /projects/:id/deployments/:deployment_id/merge_requests source_project_id unmodeled
--- PASS: TestZZWitness_SourceProjectIDOnTheFourMergeRequestLists (3.68s)
```

The stand-in tests hold both client-go states: a list option without
`merged_after` gives the finding and its `upstream` declaration answers
it; with the field it turns `not_offered`, the declaration goes stale
and the gate fails. They also hold that a member nested under a recorded
key that the stand-in's struct does not model is `unmodeled`, and that a
redirect another action follows leaves its route's params judged.

## What it finds on main, and how each is answered

On the 19.5.0 record it judges 5791 params of 1101 reached routes and
covers 2792. The summary counts findings, and a container that stands
for its members is one finding: 785 `unmodeled` on 168 routes (905
params), 79 `not_offered` on 13 (85), 523 `offered_unrecorded` on 98
(537), 3 `unjudged`, and 1445 declared (1458).

Declared: the settings update and every integration take a map
(`map-input`, 1371), rows 59 and 133 of the register (`upstream`, 42),
four `duo_*` flags GitLab drops on create (`declared-not-applied`, 8,
from `filter_duo_vulnerability_attributes!`), the GitHub payload of the
mirror pull (`webhook-payload`, 8), and every param of the four label
collection routes, which client-go takes only for a nil label argument
and our handlers never pass one (`branch-not-taken`, 16).

The 79 `not_offered` are what the next layer answers (#1596): 61
`override_params[...]` of the file import, three list flags of the
deployment merge requests, the avatar on eight create and edit actions,
and `repository_checksum_failed` and `wiki_checksum_failed` on the four
project lists, which main stopped offering in #1570 because GitLab reads
neither. The `unmodeled` backlog goes to client-go (#1598), and gating
the undeclared findings waits on it (#1599).

One finding is a defect of ours, and I left it undeclared so it stays
visible: `admin.terraform_state_lock` sends none of the seven lock
fields GitLab requires, because client-go's `TerraformStates.Lock` takes
no options, so every call is refused with `ID is missing` (#1597). No
category for a protocol body is added: the one candidate is that defect,
left visible.

The figures `request-paths.md` and `cmd-utilities.md` give in this layer
are the ones I measured before the stack was rebased onto main (613
endpoints for the always-sent check, and for R-PARAM 2794 covered, 71
`not_offered` on 9 routes and 534 `offered_unrecorded` on 99); main's
newer request record and #1570 have moved them since. The top of the
stack writes there what the merged tree measures.

## Checks

`go test ./... -count=1` with the snapshot parity deferred, and every
`cmd/audit_1to1` suite on the real tree without it. 100% statements and
every condition both ways (gobco) on `sdkoptions`, `params`,
`sdkroutes`, `paths`, `required`, `sdk` and the command. gremlins: Lived
0 and Not covered 0 on each, except `paths` (536 killed), whose 490 not
covered are package-level initializers in seven files of `paths` this
layer does not touch (`endpoint_declarations.go`, `endpoints.go`,
`orbit_declarations.go`, `pagination_declarations.go`, `published.go`,
`sent_declarations.go` and `shape_declarations.go`), which the coverage
profile does not mark. The touched `make analyze` steps pass, and
`check-action-grants-derivation` is unchanged.

---------

Signed-off-by: José M. Requena Plens <mail@jmrp.io>
jmrplens added a commit that referenced this pull request Oct 30, 2026
…ady models, and declare the ones GitLab drops (R-PARAM) (#1640)

Closes #1596
Refs #1598
Refs #1595

This layer acts on what R-PARAM found that this repository can fix
today. On its parent the scope lists 79 `not_offered` findings on 13
routes: params the option struct an action already hands client-go
models, and that no input offers. I answered every one, offering what
GitLab applies and declaring what GitLab drops or cannot apply. A
witness test pins 71 of them to that answer, and a second one the eight
checksum filters of the project lists, which main stopped offering in
#1570.

## What the check detects

`TestRun_Repository_OffersOrDeclaresTheParamsNoInputOfferedOnV3300`
reads the R-PARAM report of the real tree and pins 71 findings of
v3.30.0: the avatars, the 61 `override_params` keys and the three list
flags. Each param that is offered now has to be offered and answered by
no declaration, and each one that is declared has to be
`declared-not-applied` on a param client-go models and no input offers,
`lfs_enabled` among them, which the import input offered before this
stack. It is a witness and not a gate on the class: R-PARAM stays a
report with its gate on stale declarations, as decided, so a param
client-go starts modeling in the bump is reported as `not_offered`
beside the rest and the bump updates this list the way it updates every
witness.

The layer also stops a `declared-not-applied` or `webhook-payload`
declaration from answering a param an input offers. Before, it answered
a finding of any class, so it could hide the very defect this layer
fixes: had `lfs_enabled` come back into the import input, the
declaration would have answered the `offered_unrecorded` finding in
silence. Now the declaration goes stale and the gate fails, which I
checked by putting the field back: `POST /projects/import
override_params[lfs_enabled] (declared-not-applied): answers no
finding`.

## Witness test, red then green

On the parent (2c79544e2, with this test file and nothing else of the
layer) 72 subtests of the witness fail, the 46 params that are offered
here and the 26 that are declared, and so do the eight of
`TestRun_Repository_DeclaresTheChecksumFiltersNoProjectListReads`, one
per list and filter:

```text
$ go test -count=1 -run 'TestRun_Repository_OffersOrDeclaresTheParamsNoInputOfferedOnV3300|TestRun_Repository_DeclaresTheChecksumFiltersNoProjectListReads' ./cmd/audit_1to1/internal/params/
    analyze_test.go:622: POST /groups avatar = {Route:POST /groups Param:avatar Class:not_offered SDKModels:true Offered:false ...}, want it offered and answered by no declaration
    analyze_test.go:629: POST /projects/import override_params[topics] = {... Class:not_offered SDKModels:true Offered:false ...}, want the modeled param declared declared-not-applied and offered by no input
    analyze_test.go:629: POST /projects/import override_params[lfs_enabled] = {... Class:offered_unrecorded SDKModels:true Offered:true ...}, want the modeled param declared declared-not-applied and offered by no input
    analyze_test.go:656: GET /projects repository_checksum_failed = {Route:GET /projects Param:repository_checksum_failed Class:not_offered SDKModels:true Offered:false ...}, want the modeled param declared declared-not-applied and offered by no input
FAIL	github.com/jmrplens/gitlab-mcp-server/v3/cmd/audit_1to1/internal/params
```

and both pass here:

```text
--- PASS: TestRun_Repository_OffersOrDeclaresTheParamsNoInputOfferedOnV3300
--- PASS: TestRun_Repository_DeclaresTheChecksumFiltersNoProjectListReads
ok  	github.com/jmrplens/gitlab-mcp-server/v3/cmd/audit_1to1/internal/params
```

The class restriction has its own unit cases, red before it (`not
applied on an offered param` and `webhook payload on an offered param`,
`answers = true, want false`) and green after.

## What it found on main, and how each is answered

The avatar on seven create and edit routes: `group.create`,
`project.create`, `project.create_for_user`, `user.create`,
`user.modify`, `admin.topic_create` and `admin.topic_update` now take an
`avatar` object (the file name GitLab reads the image type from, and a
local file or base64 content), refused before any request when it cannot
be read. With it client-go sends the call as a multipart upload.
`interactive.project_create` reaches the same route, which
`project.create` now covers.

That upload writes only the first value of each field (`UploadRequest`,
`fields.Get`). So beside an avatar the project create sends its topics
and tags, and the group create its download limit allowlist, as one
comma-separated value, which GitLab splits back
(`CommaSeparatedToArray`). The group alert list is an `Array[Integer]`
GitLab does not split, so a create that gives one beside an avatar is
refused and pointed at `group.update`. The test that holds the allowlist
was red on the commit before the fix: the mock read `["alice"]` for
`alice` and `bob`. The defect is now row 151 of the upstream register,
with the three sites. The upstream wait table cannot hold it, because
its predicates read types and routes and this is the body of a function,
so `TestSDK_CreateProjectWithAvatar_WritesTheFirstTopicAlone` pins what
client-go sends and fails the day it sends more.

The 61 `override_params` keys of `project.import_from_file`: I read the
import in GitLab's source. The overrides reach the project only through
the tree restorer, which keeps the attributes
`lib/gitlab/import_export/project/import_export.yml` permits for a
project (`AttributesPermitter`, through
`Group::RelationTreeRestorer#filter_attributes`), and then hands them to
`Project#assign_attributes`. Of the 86 keys GitLab declares there it
permits 52, and two of those it cannot assign: `topics` is the project's
association with `Projects::Topic`, which a list of names does not fill,
and the project has no `jobs_enabled` writer at all, since
`lib/api/projects.rb` renames it to `builds_enabled` on the create and
the edit and never on the import. Either one raises inside the restore,
which fails the import. That is a trace of GitLab's code, which I did
not run against an instance; no e2e scenario imports with overrides. The
input now offers the 50 keys the import applies, 39 of the 61 among them
and 11 it offered before. The other 22 of the 61 are declared
`declared-not-applied`, and so are `lfs_enabled` and the 13 dropped keys
client-go does not model, 36 in all, so none of them waits on a
client-go change that would not make GitLab apply it. The input also
stopped offering five keys it offered before this stack: `name`, `path`,
`namespace_id` and `default_branch`, which Grape drops because the
import does not declare them under `override_params`, and `lfs_enabled`,
which the restorer excludes by name.

The three list flags of a deployment's merge requests (`view`,
`with_labels_details`, `with_merge_status_recheck`) are declared
`declared-not-applied`: `lib/api/deployments.rb` presents
`MergeRequestBasic` with no serializer options, so the route applies
none of them.

Two things came out of the work. `PUT /groups/:id`, `PUT /projects/:id`
and `PUT /topics/:id` remove the avatar when it is sent blank, which
client-go writes for an empty avatar, and no action did it:
`group.update`, `project.update` and `admin.topic_update` take
`remove_avatar`. And the request inventory now records the part names of
a multipart form, so an upload's options can cover a param the way a
JSON body's keys do.

## The report after

On the committed request record the scope reports `not_offered` 0 (was
79), `unmodeled` 773 (was 785) and 1491 declared (was 1445). Eight of
the declarations answer `repository_checksum_failed` and
`wiki_checksum_failed` on the four project lists, which main stopped
offering in #1570 because GitLab reads neither, and which R-PARAM would
otherwise report `not_offered`;
`TestRun_Repository_DeclaresTheChecksumFiltersNoProjectListReads` holds
them. Of the 13 dropped import keys client-go does not model, 12 left
`unmodeled` for their declaration; the thirteenth,
`duo_code_review_decisions_enabled`, was already answered by row 133,
whose text now says the import would not apply it. The params offered
here show as `offered_unrecorded` (568 on 103 routes, was 523 on 98)
until the inventory is regenerated, which the stack does once at its
top. There the recorded requests carry every param this layer offers:
covered goes from 2792 to 2864, `offered_unrecorded` falls to 496 on 92
routes, and no avatar or `override_params` key the layer offers is left
among the findings. The R-PARAM figures `request-paths.md` gives in this
layer (2794 covered, 579 `offered_unrecorded` on 104) are the ones I
measured before the stack was rebased onto main, and the top writes
there what the merged tree measures.

The `unmodeled` backlog, grouped by client-go service, is #1598.

## Checks

`go test ./... -count=1` with the snapshot parity deferred, and every
`cmd/audit_1to1` suite on the real tree without it. 100% statements and
every condition both ways (gobco) on the nine touched packages. gremlins
on the files the layer changes, the rest of each package excluded: Lived
0 and Not covered 0 in `topics`, `users`, `groups`, `projects`,
`projectimportexport`, `params` and `testutil`. The first run left one
mutant alive in the multipart recorder (`||` to `&&` on the boundary
check), and its test now holds that a form without a boundary is refused
before its body is read; `packageOutside`, a tagless switch from #598
whose case expressions gremlins could not see, is a chain of ifs now,
which took `testutil` from 3 not covered to none. The `structs` and
`adminspecs` changes are table entries and a string. `golangci-lint` is
clean on them, and the `make analyze` steps the layer touches pass, as
do markdownlint and the doc link check on the four contributor pages it
edits. The new end-to-end scenarios compile and `make check-e2e-static`
passes; I did not run them against a GitLab here. `gen_testing_docs
--check` reports the test counts stale, as it already did below this
layer, and is regenerated at the top.

---------

Signed-off-by: José M. Requena Plens <mail@jmrp.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

e2e The end-to-end suite and its Docker fixtures tooling The audit and generator commands under cmd/, and the Makefile targets that run them v3.4.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Read the 65 request parameters GitLab marks for a paid tier that a lower tier's schema offers

1 participant